//////////////////////////////////////////////
// main.cpp
//
//////////////////////////////////////////////

/// Includes ---------------------------------

// nkLog
#include <NilkinsLog/Loggers/ConsoleLogger.h>

// nkScripts
#include <NilkinsScripts/Log/LogManager.h>

#include <NilkinsScripts/Environments/Functions/Function.h>

#include <NilkinsScripts/Environments/UserTypes/ArrayAccessorDescriptor.h>
#include <NilkinsScripts/Environments/UserTypes/UserTypeFieldDescriptor.h>
#include <NilkinsScripts/Environments/UserTypes/UserType.h>

#include <NilkinsScripts/Environments/Environment.h>
#include <NilkinsScripts/Environments/EnvironmentManager.h>

#include <NilkinsScripts/Interpreters/Interpreter.h>

#include <NilkinsScripts/Scripts/Script.h>
#include <NilkinsScripts/Scripts/ScriptManager.h>

// Standards
#include <memory>

/// Structures -------------------------------

// Class used as a reference for our user type
class Data
{
	public :

		std::string _label = "I" ;
		int _i = 0 ;

		int _array [3] = {5, 4, 3} ;
} ;

/// Function ---------------------------------

int main ()
{
	// Prepare for logging
	std::unique_ptr<nkLog::Logger> logger = std::make_unique<nkLog::ConsoleLogger>() ;
	nkScripts::LogManager::getInstance()->setReceiver(logger.get()) ;

	// Create our environment
	nkScripts::Environment* env = nkScripts::EnvironmentManager::getInstance()->createOrRetrieve("firstEnv") ;
	env->setEnvironmentFor(nkScripts::INTERPRETER::LUA) ;

	// Basis for the user type
	nkScripts::UserType* type = env->setUserType("nkTutorial::Data") ;

	// Set two instances within the environment for demonstration
	Data data0 ;
	env->setObject("data0", "nkTutorial::Data", &data0) ;

	Data data1 ;
	env->setObject("data1", "nkTutorial::Data", &data1) ;

	// Override a builtin function, here equality check
	nkScripts::Function* func = type->overrideBuiltInFunction(nkScripts::TYPE_BUILT_IN_FUNCTIONS::BUILT_IN_EQ) ;

	func->setFunction
	(
		[] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue
		{
			// Two operands for this operation, left and right
			Data* data0 = (Data*)stack[0]._valUser._userData ;
			Data* data1 = (Data*)stack[1]._valUser._userData ;

			return nkScripts::OutputValue(data0->_label == data1->_label && data0->_i == data1->_i) ;
		}
	) ;

	// Now prepare the script to execute
	nkScripts::Script* script = nkScripts::ScriptManager::getInstance()->createOrRetrieve("firstScript") ;
	script->setTargetInterpreter(nkScripts::INTERPRETER::LUA) ;
	script->setSources
	(
		R"eos(
			print(data0 == data1)
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Override toString this time
	func = type->overrideBuiltInFunction(nkScripts::TYPE_BUILT_IN_FUNCTIONS::BUILT_IN_TO_STRING) ;

	func->setFunction
	(
		[] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue
		{
			// This operation has only one operand, the object to stringify
			Data* data = (Data*)stack[0]._valUser._userData ;

			std::string result = data->_label + " : " + std::to_string(data->_i) ;
			result+= " - " + std::to_string(data->_array[0]) + ", " + std::to_string(data->_array[1]) + ", " + std::to_string(data->_array[2]) ;

			return nkScripts::OutputValue(result.c_str()) ;
		}
	) ;

	script->unload() ;
	script->setSources
	(
		R"eos(
			print(data0)
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Now augment our type with fields
	nkScripts::UserTypeFieldDescriptor fieldDesc ;

	fieldDesc._fieldName = "_i" ;
	fieldDesc._fieldType = nkScripts::FUNCTION_PARAMETER_TYPE::INT ;

	fieldDesc._getter = 
		[] (void* data) -> nkScripts::OutputValue
		{
			// Cast the type to be able to use it
			Data* dataCast = (Data*)data ;

			return nkScripts::OutputValue(dataCast->_i) ;
		} ;

	fieldDesc._setter =
		[] (const nkScripts::DataStack& stack) -> void
		{
			// Like a method, we get the user data and then the value to assign
			Data* data = (Data*)stack[0]._valUser._userData ;
			data->_i = stack[1]._valInt ;

			// We do not expect any output this time, only a set
		} ;

	type->addField(fieldDesc) ;

	script->unload() ;
	script->setSources
	(
		R"eos(
			data0._i = 50 -- Uses the setter

			print(data0._i) -- Uses the getter
			print(data0)
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Demonstrate the array indexing way
	nkScripts::ArrayAccessorDescriptor accessDesc ;

	accessDesc._fieldType = nkScripts::FUNCTION_PARAMETER_TYPE::INT ;

	accessDesc._readFunc =
		[] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue
		{
			// First slot is data, second is index
			Data* data = (Data*)stack[0]._valUser._userData ;
			int index = stack[1]._valInt ;
			
			return nkScripts::OutputValue(data->_array[index]) ;
		} ;

	accessDesc._writeFunc =
		[] (const nkScripts::DataStack& stack) -> void
		{
			// First slot is data, second index, third is the value to write
			Data* data = (Data*)stack[0]._valUser._userData ;
			int index = stack[1]._valInt ;
			int value = stack[2]._valInt ;

			data->_array[index] = value ;
		} ;

	type->enableArrayIndexing(accessDesc) ;

	script->unload() ;
	script->setSources
	(
		R"eos(
			data0[0] = 55 -- Uses the setter
			data0[1] = 25

			print(data0[0]) -- Uses the getter
			print(data0)
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Small pause to be able to witness the console
	system("pause") ;

	return 0 ;
}